<?php
// +----------------------------------------------------------------------
// | ThinkCrypto
// +----------------------------------------------------------------------
// | 版权所有 2014~2021 青海云音信息技术有限公司 [ http://www.yyinfos.com ]
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// +----------------------------------------------------------------------
// | gitcode 代码仓库：https://gitcode.net/qhweb/ThinkCrypto
// +----------------------------------------------------------------------
declare(strict_types=1);

namespace think\crypto\service;

use RuntimeException;
use think\crypto\libs\AES;
use think\facade\Cache;

/*
* 加密服务核心类（支持 SM4 与 AES，多模式）
* Class Crypto
* @package think\crypto\service
*/
class Crypto
{
    // 算法与模式
    private const CIPHER_SM4 = 'sm4-%s';
    private const MODES_AES = ['cbc', 'cfb', 'ctr', 'ecb', 'gcm', 'ccm', 'ocb'];
    private const MODES_SM4 = ['ecb', 'cbc', 'cfb', 'ofb', 'ctr'];

    // app 容器
    protected $app;
    //密钥
    protected string $key = '1234567890abcdef';
    //初始向量
    public string $iv = 'abcdefghijklmnop';
    //版权表示
    public string $comments = '版权所有 2014~2021 青海云音信息技术有限公司 [ http://www.yyinfos.com ]';
    //加密模式
    protected string $type = 'sm';
    //sm4加密类型
    protected string $mode = 'cbc';
    //数据模式
    protected string $format = 'hex';

    // 安全与行为开关
    protected $sm4ModeAllowed = [
        'ecb' => false, // 建议禁用
        'cbc' => true,
        'cfb' => true,
        'ofb' => true,
        'ctr' => true,
    ];
    //需要加密的固定PHP文件名
    protected array $allowFiles = ['sys','common'];

    /** @var AES|null 延迟实例化 */
    private ?AES $aes = null;

    /**
     * 构造函数
     * Crypto constructor.
     * @param \think\App $app
     */
    public function __construct() {

        $this->app      = app();
        $cfg = $this->app->config->get('crypto', []);

        $this->format   = $cfg['format']    ?? $this->format;
        $this->key      = $cfg['key']       ?? $this->key;
        $this->iv       = $cfg['iv']        ?? $this->iv;
        $this->type     = $cfg['type']      ?? $this->type;
        $this->mode     = $cfg['mode']      ?? $this->mode;
        $this->comments = $cfg['copyright'] ?? $this->comments;
        

        // 安全建议：默认禁用 SM4-ECB
        if (isset($cfg['sm4_allow_ecb'])) {
            $this->sm4ModeAllowed['ecb'] = (bool)$cfg['sm4_allow_ecb'];
        }
        if (isset($cfg['allowFiles'])) {
            $this->allowFiles = (array)$cfg['allowFiles'];
        }
    }

    /**
     * 通用加密（指定算法与模式）
     * @param string $data 明文
     * @param string $algo 'sm4' | 'aes'
     * @param string $mode 模式
     * @param string $aad 附加认证数据（GCM/CCM/OCB）,sm加密则为返回数据类型hex、base64
     * @return string 密文（Base64）
     */
    public function encryptWithMode(string $data, string $algo, string $mode, string $aad = '', $iv = null): string
    {
        $algo = strtolower($algo);
        $mode = strtolower($mode);

        if ($algo === 'sm' && function_exists('sm4Encrypt')) {
            $this->checkSM4Mode($mode);
            $key = $this->buildSM4Key();
            $iv  = $iv ?? $this->normalizeIv();
            $mode = sprintf(self::CIPHER_SM4, $mode);
            $format = $aad ?? $this->format;
            
            return sm4Encrypt($data,$mode,$iv,$key,$format); 
        }
        if ($algo === 'aes') {
            $this->checkAESMode($mode);
            return $this->encryptAES($data, $mode, $aad);
        }
        throw new RuntimeException("不支持的算法：{$algo}");
    }
    
    /**
     * 通用解密
     * @param string $data Base64 密文
     * @param string $algo 'sm4' | 'aes'
     * @param string $mode 模式
     * @param string $aad 附加认证数据（GCM/CCM/OCB）
     * @param string $iv 向量
     * @return string 明文
     */
    public function decryptWithMode(string $data, string $algo, string $mode, string $aad = '', $iv = null): string
    {
        $algo = strtolower($algo);
        $mode = strtolower($mode);

        if ($algo === 'sm' && function_exists('sm4Decrypt')) {
            $this->checkSM4Mode($mode);
            $key = $this->buildSM4Key();
            $iv  = $iv ?? $this->normalizeIv();
            $mode = sprintf(self::CIPHER_SM4, $mode);
            $format = $aad ?? $this->format;
            return sm4Decrypt($data,$mode,$iv,$key,$format); 
        }
        if ($algo === 'aes') {
            $this->checkAESMode($mode);
            return $this->decryptAES($data, $mode, $aad);
        }
        throw new RuntimeException("不支持的算法：{$algo}");
    }

    public function encrypt($data,$iv=null):string
    {
        return empty($data) ? '' : $this->encryptWithMode($data,$this->type,$this->mode,$this->format,$iv);
    }

    /**
     * 解密
     *
     * @param [type] $data
     * @param [type] $type
     * @return string
     */
    public function decrypt($data,$iv=null):string
    {
        return empty($data) ? '' : $this->decryptWithMode($data,$this->type,$this->mode,$this->format,$iv);
    }

    // 数据签名
    public function sm3Sign($data)
    {
        if(empty($data)) return '';
        $data = !is_array($data) ?: $this->buildSignString($data);
        return sm3($data);
    }
    /**
     * 签名验证
     *
     * @param [type] $data  数据
     * @param string $sign  签名
     * @return void
     */
    public function sm3VerifySign($data, $sign=''):bool
    {
        if(empty($data) || empty($sign)) return false;
        $data = !is_array($data) ?: $this->buildSignString($data);
        return $sign === sm3($data);
    }

    /**
     * 将数组拼接成用于签名的字符串
     *
     * @param array $params 原始参数数组
     * @param array $excludeKeys 不参与签名的键名，如 ['sign', 'signature']
     * @return string 拼接后的字符串，如 age=25&city=北京&name=张三
     */
    private function buildSignString(array $params=[], array $excludeKeys = ['sign', 'signature']): string
    {
        if(empty($params)) return '';
        // 1. 过滤掉不需要参与签名的字段
        $filtered = [];
        foreach ($params as $key => $value) {
            if (!in_array(strtolower($key), array_map('strtolower', $excludeKeys))) {
                $filtered[$key] = $value;
            }
        }

        // 2. 按键名进行 ASCII 升序排序
        ksort($filtered);

        // 3. 拼接成 key=value&key2=value2 格式
        $pairs = [];
        foreach ($filtered as $k => $v) {
            // 直接拼接，如 age=25，不进行 URL 编码（如接口未要求）
            $pairs[] = "{$k}=" . $v;
        }

        // 4. 用 & 拼接，不带末尾 &
        return implode('&', $pairs);
    }

    /**
     * 判断给定的 PHP 代码字符串中是否包含 namespace 声明
     * @param string $phpCode PHP 代码（可以是文件内容字符串）
     * @return bool 是否包含 namespace 声明
     */
    private function hasNamespaceDeclaration(string $phpCode=''): bool
    {
        if(empty($phpCode)) return false;
        return preg_match('/\bnamespace\b\s+/i', $phpCode) === 1;
    }

    /**
     * 解密一个加密的 PHP 文件
     * @param string $path 加密的文件地址
     * @return string 解密后的文件内容
     */
    public function decryptFile(string $filepath): string
    {
        //1. 生成定义的变量名
        $defined = 'YYCMS_'.strtoupper(md5($filepath));
        
        //2. 检查文件是否存在）
        if (empty($filepath) || !file_exists($filepath)) {
            return '';
        }
        
        try {
            //3. 引入加密文件
            require_once $filepath;
            //4.解密数据
            if(!defined($defined)){
                return '';
            }
           
            $content = constant($defined);
            
            $iv    = substr($content, 0,16);
            $content = $this->decrypt(substr($content, 16),$iv);
            // 移除的 SOH
            $content = preg_replace('/\x01/', '', $content);
            //5. 返回解密后的内容，对变量进行混淆
            return rtrim($content);
        } catch (\Exception $e) {
            return '';
        }  
    }

    /**
     * 加密一个加密的 PHP 文件
     * @param string $path 加密的文件地址
     * @return string 解密后的文件内容
     */
    public function encryptFile(string $filepath, $toPath=null): string
    {
        //1. 检查文件是否存在）
        if (empty($filepath) || !file_exists($filepath)) {
            return '';
        }
        //2. 生成定义的变量名
        $toPath = $toPath ?? $filepath;
        $defined  = 'YYCMS_'.strtoupper(md5($toPath));
        $content  = file_get_contents($filepath);
        $basename = basename($filepath,'.php');
        $isEncode = strpos($content,$this->comments) > 0;
        $iv       = $this->random();
        $date       = date('Y-m-d H:i:s');
        $year       = date('Y');
        $comments = "// +----------------------------------------------------------------------
// | 系统名称: YYCMS. 授权{$this->getHost()}使用
// +----------------------------------------------------------------------
// | 版权所有 (c) 2020~{$year} {$this->comments}. 保留所有权利。
// +----------------------------------------------------------------------
// | 作者: qhweb <qhweb@foxmail.com>
// +----------------------------------------------------------------------
// | 创建时间: {$date}
// +----------------------------------------------------------------------";
        if(!$isEncode && ($this->hasNamespaceDeclaration($content) || in_array($basename,$this->allowFiles))){
            $encoded    = $this->encrypt($content,$iv);
            $header     = "<?php\n/*\n{$comments}\n*/\n";
            $defineLine = "if (!defined('{$defined}')) {\n    define('{$defined}', '{$iv}{$encoded}'); \n}\n";
            $footer     = "?>";
            return $header . $defineLine . $footer;
        }else{
            return '';
        }
    }

    /**
     * 加密文件夹
     *
     * @param string $scoure  来源
     * @param string $output  输出
     * @return void
     */
    public function encryptDir(string $from,string $to,$output=null){
        //待加密文件
        $from = $this->app->getRootPath().$from .DIRECTORY_SEPARATOR;
        $to = $this->app->getRootPath().$to .DIRECTORY_SEPARATOR;
        //创建输出文件夹
        if(!is_dir($to)){
            mkdir($to,0755);
        }
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($from, \RecursiveDirectoryIterator::SKIP_DOTS)
        );
        //循环加密文件并加密
        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $path = $file->getRealPath();
                //保存路径
                $filePath = str_replace($from,$to,$path);
                //创建目录
                if(!is_dir($dirname = dirname($filePath))){
                    mkdir($dirname,0755,true);
                }
                //加密或复制文件
                if ($path && str_ends_with($path, '.php')) {
                    $encodContent = $this->encryptFile($path,$filePath);
                    if(!empty($encodContent) && file_put_contents($filePath, $encodContent)){
                        $this->sendMsg("加密文件：{$path}完成！",$output);
                    }else{
                        copy($path, $filePath);
                        $this->sendMsg("复制文件：{$path}完成！",$output);
                    }
                }else{
                    copy($path, $filePath);
                    $this->sendMsg("复制文件：{$path}完成！",$output);
                }
            }
        }
        $this->sendMsg("全部任务完成！",$output);

        //安全提醒
        if($from !== $to){
            $this->sendMsg("==========",$output);
            $this->sendMsg("防止代码被窃取，提醒你删除目录:{$from}",$output);
            $this->sendMsg("==========",$output);
        }
    }

    /**
     * 解密文件夹
     *
     * @param string $scoure  来源
     * @param string $output  输出
     * @return void
     */
    public function decryptDir(string $from,string $to,$output=null){
        $from = $this->app->getRootPath().$from .DIRECTORY_SEPARATOR;
        $to = $this->app->getRootPath().$to .DIRECTORY_SEPARATOR;
       
        if(!is_dir($to)){
            mkdir($to,0755);
        }
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($from, \RecursiveDirectoryIterator::SKIP_DOTS)
        );
        //循环加密文件并加密
        foreach ($iterator as $file) {
            if ($file->isFile()) {
                $path = $file->getRealPath();
                $filePath = str_replace($from,$to,$path);
                
                //创建目录
                if(!is_dir($dirname = dirname($filePath))){
                    mkdir($dirname,0755,true);
                }
                //解密或复制文件
                if ($path && str_ends_with($path, '.php')) {
                    
                    $content = $this->decryptFile($path);
                    // halt($path,$content);
                    // 增强加密标识检测
                    if (!empty($content) &&  file_put_contents($filePath, $content)) { 
                        $this->sendMsg("解密文件：{$path}完成！",$output);
                    }else{
                        copy($path, $filePath);
                        $this->sendMsg("复制文件：{$path}完成！",$output);
                    }
                }else{
                    copy($path, $filePath);
                    $this->sendMsg("复制文件：{$path}完成！",$output);
                }
            }
        }

        //删除解密后的换成文件
        $cacheFiles = Cache::get('decryptedFile');
        foreach ($cacheFiles as $val) {
            if(file_exists($val)) @unlink($val);
        }
        $this->sendMsg('全部任务完成！',$output); 
    }

    /**
     * 输出信息到窗口
     *
     * @param [type] $str
     * @param [type] $output
     * @return void
     */
    private function sendMsg($str,$output=null){
        if ($output) {
            $output->writeln("<comment>{$str}</comment>");
        } else {
            echo "<p>{$str}</p>";
            // 刷新 PHP 输出缓冲区
            ob_flush();
            // 通知服务器将数据推送给客户端
            flush();
        }
    }

    protected function buildSM4Key(): string
    {
        return substr(md5($this->key),16);
    }
    protected function normalizeIv() : string {
        return substr(md5($this->iv),0,16);
    }

    protected function checkSM4Mode(string $mode): void
    {
        if (!in_array($mode, self::MODES_SM4, true)) {
            throw new RuntimeException("不支持的 SM4 模式：{$mode}");
        }
        if (!$this->sm4ModeAllowed[$mode]) {
            throw new RuntimeException("SM4-{$mode} 被禁用，请在配置中显式开启");
        }
    }
    protected function checkAESMode(string $mode): void
    {
        if (!in_array($mode, self::MODES_AES, true)) {
            throw new RuntimeException("不支持的 AES 模式：{$mode}");
        }
    }
    protected function encryptAES(string $data, string $mode, string $aad = ''): string
    {
        return $this->aes()->encryptWithMode($data, $mode, $aad);
    }

    protected function decryptAES(string $data, string $mode, string $aad = ''): string
    {
        return $this->aes()->decryptWithMode($data, $mode, $aad);
    }

    private function aes(): AES
    {
        if ($this->aes === null) {
            // 使用 SM3 派生 32 字节密钥，保持与 SM 分支一致的安全强度
            $this->aes = new AES(md5($this->key),$this->format);
        }
        return $this->aes;
    }
    private function random(int $length = 16): string {
        $chars = $this->format == 'hex' ? '0123456789abcdef' : 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $len   = strlen($chars) - 1;
        $res   = '';
        for ($i = 0; $i < $length; $i++) {
            $res .= $chars[mt_rand(0, $len)];
        }
        return $res;
    }
    /**
     * 获取当前 URL
     */
    private function getHost(string $url = ''): ?string
    {
        // 1) 确定主机名
        if ($url === '') {
            // 优先使用当前请求的 Host（受端口影响，先去除端口）
            $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '';
            $host = preg_replace('/:\d+$/', '', $host); // 去掉 :80/:443
        } else {
            // 解析传入的 URL 或主机名
            $parsed = parse_url($url);
            if (isset($parsed['host'])) {
                $host = $parsed['host'];
            } else {
                // 可能是裸域名或 IP
                $host = trim($url, " \t\n\r\0\x0B/");
            }
        }

        return $host;
    }
}
